/*
 * This file is part of the xOperator SPARQL/XMPP agent.
 * For further information see: http://xoperator.aksw.org
 * Copyright (C) 2007-2008  Jörg Unbehauen

 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.

 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.

 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package org.aksw.xoperator.xmpp;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ThreadPoolExecutor;

import org.aksw.xoperator.Constants;
import org.aksw.xoperator.conf.AgentConfiguration;
import org.aksw.xoperator.sparql.p2p.SPARQLQueryOverXmpp;
import org.aksw.xoperator.sparql.p2p.SPARQLResultoverXmpp;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jivesoftware.smack.Chat;
import org.jivesoftware.smack.ChatManagerListener;
import org.jivesoftware.smack.ConnectionConfiguration;
import org.jivesoftware.smack.MessageListener;
import org.jivesoftware.smack.PacketCollector;
import org.jivesoftware.smack.PacketListener;
import org.jivesoftware.smack.Roster;
import org.jivesoftware.smack.RosterEntry;
import org.jivesoftware.smack.RosterListener;
import org.jivesoftware.smack.SmackConfiguration;
import org.jivesoftware.smack.XMPPConnection;
import org.jivesoftware.smack.XMPPException;
import org.jivesoftware.smack.filter.PacketFilter;
import org.jivesoftware.smack.filter.PacketIDFilter;
import org.jivesoftware.smack.packet.IQ;
import org.jivesoftware.smack.packet.Message;
import org.jivesoftware.smack.packet.Packet;
import org.jivesoftware.smack.packet.Presence;
import org.jivesoftware.smack.provider.ProviderManager;
import org.jivesoftware.smackx.MessageEventManager;
import org.jivesoftware.smackx.ServiceDiscoveryManager;
import org.jivesoftware.smackx.packet.VCard;
import org.picocontainer.Startable;


/**
 * A component managing everything needed to connect to the Jabber network
 * 
 * @author joerg
 */
public class JabberClientManager implements Startable, IJabberP2P {
	private static Log log = LogFactory.getLog(JabberClientManager.class);

	private final Map<String, VCard> vcards = new ConcurrentHashMap<String, VCard>();

	private ThreadPoolExecutor executor;

	private List<RosterListener> rosterListener = new ArrayList<RosterListener>();

	private List<ChatManagerListener> chatManagerListener = new ArrayList<ChatManagerListener>();

	private AgentConfiguration agentconf;

	private JabberClient main;

	private JabberClient proxy;

	private PacketListener incomingPacketListener;

	public JabberClientManager(AgentConfiguration agentconf,
			ThreadPoolExecutor executor) {
		super();
		SmackConfiguration.setPacketReplyTimeout(20000);
		ProviderManager.getInstance().addIQProvider("query",
				Constants.SPARQL_XMPP_SPEC_URL,
				new SPARQLQueryOverXmpp.Provider());
		ProviderManager.getInstance().addIQProvider("sparql-result",
				Constants.SPARQL_XMPP_SPEC_URL,
				new SPARQLResultoverXmpp.Provider());
		Roster.setDefaultSubscriptionMode(Roster.SubscriptionMode.manual);

		this.agentconf = agentconf;
		this.executor = executor;
	}

	/**
	 * starts the Jabber client according to the data from the configuration
	 */
	public void start() {
		main = new JabberClient(agentconf.getJabberClientConfiguration()
				.getMainAccount());
		main.connect();
		if (!agentconf.getJabberClientConfiguration().isNoProxy()) {
			main.login(true);
		} else {
			main.login(false);
		}

		enableP2P();

		// determines, if people can add themselves automatically to the roster
		if (agentconf.getJabberClientConfiguration().isAllowUnknownUsers()) {
			main.getRoster().setSubscriptionMode(
					Roster.SubscriptionMode.accept_all);
		}

		for (RosterListener listener : this.rosterListener) {
			main.registerListener(listener);
		}

		if (agentconf.getJabberClientConfiguration().isNoProxy()) {
			// for group agents there will be no proxy account
			for (ChatManagerListener listener : chatManagerListener) {
				main.registerListener(listener);
			}

		} else {
			// personal agent with separate account for communication
			proxy = new JabberClient(agentconf.getJabberClientConfiguration()
					.getProxyAccount());
			proxy.connect();
			proxy.login(false);

			for (ChatManagerListener listener : chatManagerListener) {
				proxy.registerListener(listener);
			}

			//queryForRosterInformation();

			// make sure that proxy and main are subscribed to each other
			introduceToEachOther(main, proxy);
			introduceToEachOther(proxy, main);
		}

	}

	/**
	 * guarantees that the main and the proxy account are visible to each other
	 * 
	 */
	private static void introduceToEachOther(JabberClient first,
			JabberClient second) {
		try {
			if (!first.getRoster().contains(second.accountInfo.getJid())) {
				// first does not know second, first adds to the roster, seconds
				// sends permission

				first.getRoster().createEntry(second.accountInfo.getJid(),
						second.accountInfo.getJid(), null);

				Presence subscribed = new Presence(Presence.Type.subscribed);
				subscribed.setTo(first.accountInfo.getJid());
				second.connection.sendPacket(subscribed);

			}

		} catch (XMPPException e) {
			log
					.error("Error while creating the subscriptions between agent and main account");
		}

	}

	/**
	 * DOCUMENT ME!
	 */
	public void stop() {
		// TODO Auto-generated method stub
	}



	/**
	 * DOCUMENT ME!
	 * 
	 * @param toJid
	 *            DOCUMENT ME!
	 * @param iq
	 *            DOCUMENT ME!
	 * 
	 * @return DOCUMENT ME!
	 */
	public String sendIQ(String toJid, IQ iq) {
		return main.sendIQ(toJid, iq);
	}

	/**
	 * DOCUMENT ME!
	 * 
	 * @return DOCUMENT ME!
	 */
	public Roster getRoster() {
		return main.getRoster();
	}

	/**
	 * DOCUMENT ME!
	 * 
	 * @param rosterListener
	 *            DOCUMENT ME!
	 */
	public void addRosterListener(RosterListener rosterListener) {
		this.rosterListener.add(rosterListener);
	}

	/**
	 * DOCUMENT ME!
	 * 
	 * @param chatManagerListener
	 *            DOCUMENT ME!
	 */
	public void addChatManagerListener(ChatManagerListener chatManagerListener) {
		this.chatManagerListener.add(chatManagerListener);
	}



	/**
	 * DOCUMENT ME!
	 * 
	 * @param packet
	 *            DOCUMENT ME!
	 */
	public void sendPacket(Packet packet) {
		main.connection.sendPacket(packet);
	}

	/**
	 * DOCUMENT ME!
	 * 
	 * @param filter
	 *            DOCUMENT ME!
	 * 
	 * @return DOCUMENT ME!
	 */
	public PacketCollector createPacketCollector(PacketFilter filter) {
		return main.connection.createPacketCollector(filter);
	}

	/**
	 * DOCUMENT ME!
	 * 
	 * @param incomingQueryListener
	 *            DOCUMENT ME!
	 */
	public void registerP2PListener(PacketListener incomingQueryListener) {
		this.incomingPacketListener = incomingQueryListener;
	}

	/**
	 * DOCUMENT ME!
	 */
	private void enableP2P() {
		ServiceDiscoveryManager sdm = ServiceDiscoveryManager
				.getInstanceFor(main.connection);
		sdm.addFeature(Constants.SPARQL_XMPP_SPEC_URL);
		this.main.connection.addPacketListener(incomingPacketListener,
				new SPARQLQueryOverXmpp.ClassFilter());
	}

	/**
	 * DOCUMENT ME!
	 * 
	 * @return DOCUMENT ME!
	 */
	public MessageEventManager getEventManager() {
		return new MessageEventManager(main.connection);
	}

	private ChatManagerListener createChatResponder() {
		return new ChatManagerListener() {
			public void chatCreated(Chat chat, boolean createdLocally) {
				chat.addMessageListener(new MessageListener() {
					public void processMessage(Chat chat, Message message) {
						if (message.getBody() != null
								&& message.getBody().length() > 0) {
							if (message.getBody() != null
									&& message.getBody().length() > 0) {

								log.warn("Received a message from :"
										+ message.getFrom() + " telling: "
										+ message.getBody());

								Message mess = new Message();
								mess.setType(Message.Type.error);
								mess.setBody(agentconf
										.getSecurityConfiguration()
										.getAccessDeniedChatMessage());

								try {
									chat.sendMessage(mess);
								} catch (XMPPException e) {
									log.error("Unable to reply chat", e);
								}
							}
						}

					}
				});
			}
		};
	}

	class JabberClient {
		private Log log = LogFactory.getLog(JabberClient.class);

		private JabberAccount accountInfo;

		private XMPPConnection connection;

		public JabberClient(JabberAccount account) {
			super();
			this.accountInfo = account;
		}

		/* (non-Javadoc)
		 * @see org.aksw.xoperator.xmpp.IJabberP2P#registerListener(java.lang.Object)
		 */
		public void registerListener(Object listener) {
			if (listener instanceof ChatManagerListener) {
				connection.getChatManager().addChatListener(
						(ChatManagerListener) listener);
			}

			if (listener instanceof RosterListener) {
				connection.getRoster().addRosterListener(
						(RosterListener) listener);
			}
		}

		public void addToRoster(String jid, String nick) {
			try {
				if (!getRoster().contains(jid)) {
					getRoster().createEntry(jid, nick, null);
				}

				Presence pres = new Presence(Presence.Type.subscribed);
				pres.setTo(jid);
				connection.sendPacket(pres);
			} catch (XMPPException e) {
				log.error("Error adding to Roster: ", e);
			}
		}

		/**
		 * 
		 * @return the roster of the connection
		 */
		public Roster getRoster() {
			return connection.getRoster();
		}

		/**
		 * logs into the account specified by the agent configuration. Won't
		 * work if the <code>connect()</code> method has not been called
		 * beforehand.
		 * 
		 * @param hide
		 *            determines if the presence of the connection should be xa &
		 *            low priority (true) or available & high priority (false)
		 */
		public void login(boolean hide) {
			if (log.isDebugEnabled()) {
				registerLogListeners();
			}

			try {
				this.connection.login(this.accountInfo.getJid().split("@")[0],
						this.accountInfo.getPassword(), this.accountInfo
								.getResource(), false);

				// send presence according
				if (hide) {
					this.connection
							.sendPacket(new Presence(Presence.Type.available,
									this.accountInfo.getStatusMessage(), -10,
									Presence.Mode.xa));

					for (ChatManagerListener chatList : this.connection
							.getChatManager().getChatListeners()) {
						log.info("removed a chatlistener");
						this.connection.getChatManager().removeChatListener(
								chatList);
					}

					this.connection.getChatManager().addChatListener(
							createChatResponder());
				} else {
					this.connection.sendPacket(new Presence(
							Presence.Type.available, this.accountInfo
									.getStatusMessage(), 5,
							Presence.Mode.available));
				}
			} catch (XMPPException e) {
				log
						.error("Unable to log in, please check username/password for account:"
								+ accountInfo.getJid()
								+ " .Server message was:"
								+ e.getLocalizedMessage());
			}
		}

		private void registerLogListeners() {
			this.connection.addPacketListener(new PacketListener() {
				public void processPacket(Packet packet) {
					log.debug("receiving that: " + packet.toXML());
				}
			}, new PacketFilter() {
				public boolean accept(Packet arg0) {
					return true;
				}
			});
			this.connection.addPacketWriterListener(new PacketListener() {
				public void processPacket(Packet packet) {
					log.debug("sending this: " + packet.toXML());
				}
			}, new PacketFilter() {
				public boolean accept(Packet arg0) {
					return true;
				}
			});
		}

		/**
		 * create the connection to the server
		 * 
		 * @throws XMPPException
		 */
		public void connect() {
			String server = this.accountInfo.getServer();
			String username = this.accountInfo.getJid();
			int port = (this.accountInfo.getPort() != null) ? this.accountInfo
					.getPort()
					: org.aksw.xoperator.Constants.DEFAULT_JABBER_PORT;

			if (((server == null) || server.isEmpty()) && (username != null)) {
				server = username.split("@")[1];
			}

			ConnectionConfiguration conf = new ConnectionConfiguration(server,
					port, username.split("@")[1]);
			this.connection = new XMPPConnection(conf);

			try {
				this.connection.connect();
			} catch (XMPPException e1) {
				log
						.error("Unable to connect to the server, please check server settings for account: "
								+ accountInfo.getJid()
								+ " .Server message was:"
								+ e1.getLocalizedMessage());
			}
		}

		public void logoff() {
			if (this.connection != null) {
				this.connection.disconnect();
			}
		}

		/* (non-Javadoc)
		 * @see org.aksw.xoperator.xmpp.IJabberP2P#sendIQ(java.lang.String, org.jivesoftware.smack.packet.IQ)
		 */
		public String sendIQ(String jid, IQ iq) {
			connection.sendPacket(iq);

			return iq.getPacketID();
		}

		
	}
	
	/**
	 * sends a query and waits for the answer. Blocking, no thread pooling.
	 * @param query
	 * @return
	 * @throws XMPPException 
	 */
	public IQ queryIQ(IQ query) throws XMPPException{
        // Create a packet collector to listen for a response.
        PacketCollector collector =
            main.connection.createPacketCollector(new PacketIDFilter(query.getPacketID()));

        main.connection.sendPacket(query);

        // Wait up to 5 seconds for a result.
        IQ result = (IQ) collector.nextResult(SmackConfiguration.getPacketReplyTimeout());
        // Stop queuing results
        collector.cancel();
        if (result == null) {
            throw new XMPPException("No response from the server.");
        }
        if (result.getType() == IQ.Type.ERROR) {
            throw new XMPPException(result.getError());
        }
        return result;
	}

	public XMPPConnection getConnection() {
		// TODO Auto-generated method stub
		return main.connection;
	}

}
